Skip to content

[SPARK-57891][CORE] Add CredentialProvider SPI and ServiceLoader discovery#57191

Open
yadavay-amzn wants to merge 3 commits into
apache:masterfrom
yadavay-amzn:SPARK-57891
Open

[SPARK-57891][CORE] Add CredentialProvider SPI and ServiceLoader discovery#57191
yadavay-amzn wants to merge 3 commits into
apache:masterfrom
yadavay-amzn:SPARK-57891

Conversation

@yadavay-amzn

@yadavay-amzn yadavay-amzn commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This is task 2 of the OIDC Credential Propagation SPIP (SPARK-57703), building on the core types added in SPARK-57890. It introduces the pluggable provider SPI in org.apache.spark.security (spark-core):

  • CredentialProvider - a @DeveloperApi interface that providers (AWS STS, Azure, GCP, etc.) implement to exchange a user identity for a short-lived ServiceCredential
  • CredentialResolutionException - a @DeveloperApi checked exception thrown by resolve.
  • CredentialProviderLoader - discovers implementations via ServiceLoader and selects a provider per scheme. Scheme keys are normalized to lowercase. Selection follows an explicit-configuration policy: spark.security.credentials.provider.<scheme> names the fully-qualified provider class to use for that scheme. If exactly one discovered provider supports a scheme, no configuration is needed; if multiple support it and the conf is unset, a clear error is raised listing the candidates. Each selected provider is initialized exactly once.

A FakeCredentialProvider (and a second one sharing a scheme) plus a META-INF/services registration are added under core/src/test to exercise discovery and selection.

Why are the changes needed?

There is currently no pluggable SPI for exchanging an identity token for short-lived service credentials. This is the extension point the credential-propagation framework builds on. See the SPIP design document, Appendix A.

Does this PR introduce any user-facing change?

No behavior change. It adds new @DeveloperApi types only; nothing uses them until the follow-up (SPARK-57892 / the manager task).

How was this patch tested?

New JUnit CredentialProviderLoaderSuite covering the acceptance criteria, ServiceLoader discovery, provider selection/ambiguity, error cases, and null-input guards.

Design notes (feedback/suggestions welcome)

Two choices worth considering:

  • Binding policy: explicit selection via spark.security.credentials.provider.<scheme> (single fully-qualified class name). This can be extended to an ordered-priority list later without breaking the conf if preferred.
  • init lifecycle: each provider is initialized once, on first selection, with that call's configuration (first-conf-wins), inside the loader's lock. This sets the provider lifecycle the manager task will consume.

Was this patch authored or co-authored using generative AI tooling?

No.

// Initialize exactly once under the lock (first-conf-wins).
synchronized (CredentialProviderLoader.class) {
if (!initializedProviders.contains(selected)) {
selected.init(conf);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a security concern here. The full Spark configuration map is passed to init(). Since providers are discovered via ServiceLoader (potentially third-party JARs), a buggy or malicious provider receives all config properties including secrets from other subsystems (e.g., spark.authenticate.secret, database passwords, cloud keys).

Spark has precedent for namespace scoping. DataSourceV2Utils.extractSessionConfigs() filters the full conf to only spark.datasource.<keyPrefix>.* before passing to providers. Similar to this approach, can we filter the map to only pass spark.security.credentials.* keys?

* @throws IllegalStateException if a provider returns null from {@code supportedSchemes()}
*/
public static Optional<CredentialProvider> providerFor(String scheme, Map<String, String> conf) {
Objects.requireNonNull(scheme, "scheme must not be null");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Null scheme is properly rejected, but an empty string "" passes through and silently returns Optional.empty(). An empty scheme is never valid per URI semantics (RFC 3986) and is likely always a caller bug. Can we add the following check?

if (scheme.isEmpty()) {
  throw new IllegalArgumentException("scheme must not be empty");
}

* @throws CredentialResolutionException if the credential exchange fails
* @since 4.3.0
*/
ServiceCredential resolve(UserContext user, URI target) throws CredentialResolutionException;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since CredentialProviderLoader caches and reuses provider instances (singleton), resolve() may be called from multiple threads (e.g., credential refresh scheduling). Can we document whether implementations must be thread-safe ? (interface-level or resolve() Javadoc)

Suggestion for interface-level Javadoc:

Implementations must be thread-safe: {@code resolve()} may be called concurrently
from multiple threads after {@code init()} completes.

// Initialize exactly once under the lock (first-conf-wins).
synchronized (CredentialProviderLoader.class) {
if (!initializedProviders.contains(selected)) {
selected.init(conf);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if init() throws? Currently the provider is not added to initializedProviders, so a subsequent call would retry init(). This retry behavior seems reasonable, but:

  • The provider instance may be in a half-initialized state after a failed init(). Is that safe to retry?
  • Should the contract document whether init() is expected to be idempotent-safe (i.e., safe to call again after a previous failure)?

How about:

  1. Documenting the retry-on-failure behavior in the init() Javadoc (e.g., "If init() throws, it may be retried on the next resolution attempt. Implementations should be safe to call again after a prior failure."), or
  2. Adding a test verifying this behavior

@sarutak

sarutak commented Jul 13, 2026

Copy link
Copy Markdown
Member

Let's remove the "Design notes (feedback/suggestions welcome)" section from the description because it's used as commit log when this PR is merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants